import Cesium from '../Ces/Cesium'
import WeatherVolumeConeGeometryLibrary from './WeatherVolumeConeGeometryLibrary';

let radiusScratch;

let normalScratch;

let bitangentScratch;

let tangentScratch;

let positionScratch;

/**
 * A description of a cylinder.
 *
 * @alias WeatherVolumeConeGeometry
 * @constructor
 *
 * @param {Object} options Object with the following properties:
 * @param {Number} options.length The length of the cylinder.
 * @param {Number} options.topRadius The radius of the top of the cylinder.
 * @param {Number} options.bottomRadius The radius of the bottom of the cylinder.
 * @param {Number} [options.slices=128] The number of edges around the perimeter of the cylinder.
 * @param {Cesium.VertexFormat} [options.vertexFormat=Cesium.VertexFormat.DEFAULT] The vertex attributes to be computed.
 *
 * @exception {Cesium.DeveloperError} options.slices must be greater than or equal to 3.
 *
 * @see WeatherVolumeConeGeometry.createGeometry
 *
 * @example
 * // create cylinder geometry
 * var cylinder = new Cesium.WeatherVolumeConeGeometry({
 *     length: 200000,
 *     topRadius: 80000,
 *     bottomRadius: 200000,
 * });
 * var geometry = Cesium.WeatherVolumeConeGeometry.createGeometry(cylinder);
 */


function WeatherVolumeConeGeometry(options) {
  radiusScratch = new Cesium.Cartesian2();
  normalScratch = new Cesium.Cartesian3();
  bitangentScratch = new Cesium.Cartesian3();
  tangentScratch = new Cesium.Cartesian3();
  positionScratch = new Cesium.Cartesian3();

  options = options?? Cesium.Frozen.EMPTY_OBJECT

  const length = options.length;
  const topRadius = options.topRadius;
  const bottomRadius = options.bottomRadius;
  const vertexFormat = options.vertexFormat?? Cesium.VertexFormat.DEFAULT
  const slices = options.slices?? 128

  // >>includeStart('debug', pragmas.debug);
  if (!Cesium.defined(length)) {
    throw new Cesium.DeveloperError('options.length must be Cesium.defined.');
  }
  if (!Cesium.defined(topRadius)) {
    throw new Cesium.DeveloperError('options.topRadius must be Cesium.defined.');
  }
  if (!Cesium.defined(bottomRadius)) {
    throw new Cesium.DeveloperError('options.bottomRadius must be Cesium.defined.');
  }
  if (slices < 3) {
    throw new Cesium.DeveloperError(
      'options.slices must be greater than or equal to 3.',
    );
  }
  if (
    Cesium.defined(options.offsetAttribute)
    && options.offsetAttribute === Cesium.GeometryOffsetAttribute.TOP
  ) {
    throw new Cesium.DeveloperError(
      'Cesium.GeometryOffsetAttribute.TOP is not a supported options.offsetAttribute for this geometry.',
    );
  }
  // >>includeEnd('debug');

  this._length = length;
  this._topRadius = topRadius;
  this._bottomRadius = bottomRadius;
  this._vertexFormat = Cesium.VertexFormat.clone(vertexFormat);
  this._slices = slices;
  this._offsetAttribute = options.offsetAttribute;
  this._workerName = 'createVolumeConeGeometry';
}

/**
 * Stores the provided instance into the provided array.
 *
 * @param {WeatherVolumeConeGeometry} value The value to pack.
 * @param {Number[]} array The array to pack into.
 * @param {Number} [startingIndex=0] The index into the array at which to start packing the elements.
 *
 * @returns {Number[]} The array that was packed into
 */
WeatherVolumeConeGeometry.pack = function (value, array, startingIndex) {
  // >>includeStart('debug', pragmas.debug);
  if (!Cesium.defined(value)) {
    throw new Cesium.DeveloperError('value is required');
  }
  if (!Cesium.defined(array)) {
    throw new Cesium.DeveloperError('array is required');
  }
  // >>includeEnd('debug');

  startingIndex = startingIndex?? 0

  Cesium.VertexFormat.pack(value._vertexFormat, array, startingIndex);
  startingIndex += Cesium.VertexFormat.packedLength + 5;

  array[startingIndex++] = value._length;
  array[startingIndex++] = value._topRadius;
  array[startingIndex++] = value._bottomRadius;
  array[startingIndex++] = value._slices;
  array[startingIndex] = value._offsetAttribute?? -1

  return array;
};

/**
 * Retrieves an instance from a packed array.
 *
 * @param {Number[]} array The packed array.
 * @param {Number} [startingIndex=0] The starting index of the element to be unpacked.
 * @param {WeatherVolumeConeGeometry} [result] The object into which to store the result.
 * @returns {WeatherVolumeConeGeometry} The modified result parameter or a new WeatherVolumeConeGeometry instance if one was not provided.
 */
WeatherVolumeConeGeometry.unpack = function (array, startingIndex, result) {
  // >>includeStart('debug', pragmas.debug);
  if (!Cesium.defined(array)) {
    throw new Cesium.DeveloperError('array is required');
  }
  // >>includeEnd('debug');


  const scratchVertexFormat = new Cesium.VertexFormat();
  const scratchOptions = {
    vertexFormat: scratchVertexFormat,
    length: undefined,
    topRadius: undefined,
    bottomRadius: undefined,
    slices: undefined,
    offsetAttribute: undefined,
  };


  startingIndex = startingIndex ?? 0

  const vertexFormat = Cesium.VertexFormat.unpack(
    array,
    startingIndex,
    scratchVertexFormat,
  );

  startingIndex += Cesium.VertexFormat.packedLength;

  const length = array[startingIndex++];
  const topRadius = array[startingIndex++];
  const bottomRadius = array[startingIndex++];
  const slices = array[startingIndex++];
  const offsetAttribute = array[startingIndex];

  if (!Cesium.defined(result)) {
    scratchOptions.length = length;
    scratchOptions.topRadius = topRadius;
    scratchOptions.bottomRadius = bottomRadius;
    scratchOptions.slices = slices;
    scratchOptions.offsetAttribute = offsetAttribute === -1 ? undefined : offsetAttribute;

    return new WeatherVolumeConeGeometry(scratchOptions);
  }

  result._vertexFormat = Cesium.VertexFormat.clone(vertexFormat, result._vertexFormat);
  result._length = length;
  result._topRadius = topRadius;
  result._bottomRadius = bottomRadius;
  result._slices = slices;
  result._offsetAttribute = offsetAttribute === -1 ? undefined : offsetAttribute;

  return result;
};

/**
 * Computes the geometric representation of a cylinder, including its vertices, indices, and a bounding sphere.
 *
 * @param {WeatherVolumeConeGeometry} WeatherVolumeConeGeometry A description of the cylinder.
 * @returns {Cesium.Geometry|undefined} The computed vertices and indices.
 */
WeatherVolumeConeGeometry.createGeometry = function (weatherVolumeConeGeometry) {
  let length = weatherVolumeConeGeometry._length;
  const topRadius = weatherVolumeConeGeometry._topRadius;
  const bottomRadius = weatherVolumeConeGeometry._bottomRadius;
  const vertexFormat = weatherVolumeConeGeometry._vertexFormat;
  const slices = weatherVolumeConeGeometry._slices;

  if (
    length <= 0
    || topRadius < 0
    || bottomRadius < 0
    || (topRadius === 0 && bottomRadius === 0)
  ) {
    return;
  }

  const twoSlices = slices + slices;
  const threeSlices = slices + twoSlices;
  const numVertices = twoSlices + twoSlices;

  const positions = WeatherVolumeConeGeometryLibrary.computePositions(
    length,
    topRadius,
    bottomRadius,
    slices,
    true,
  );

  const st = vertexFormat.st ? new Float32Array(numVertices * 2) : undefined;
  const normals = vertexFormat.normal
    ? new Float32Array(numVertices * 3)
    : undefined;
  const tangents = vertexFormat.tangent
    ? new Float32Array(numVertices * 3)
    : undefined;
  const bitangents = vertexFormat.bitangent
    ? new Float32Array(numVertices * 3)
    : undefined;

  let i;
  const computeNormal = vertexFormat.normal || vertexFormat.tangent || vertexFormat.bitangent;

  if (computeNormal) {
    const computeTangent = vertexFormat.tangent || vertexFormat.bitangent;

    let normalIndex = 0;

    let tangentIndex = 0;

    let bitangentIndex = 0;

    const theta = Math.atan2(bottomRadius - topRadius, length);
    const normal = normalScratch;

    normal.z = Math.sin(theta);
    const normalScale = Math.cos(theta);

    let tangent = tangentScratch;

    let bitangent = bitangentScratch;

    for (i = 0; i < slices; i++) {
      const angle = (i / slices) * Cesium.Math.TWO_PI;
      const x = normalScale * Math.cos(angle);
      const y = normalScale * Math.sin(angle);

      if (computeNormal) {
        normal.x = x;
        normal.y = y;

        if (computeTangent) {
          tangent = Cesium.Cartesian3.normalize(
            Cesium.Cartesian3.cross(Cesium.Cartesian3.UNIT_Z, normal, tangent),
            tangent,
          );
        }

        if (vertexFormat.normal) {
          normals[normalIndex++] = normal.x;
          normals[normalIndex++] = normal.y;
          normals[normalIndex++] = normal.z;
          normals[normalIndex++] = normal.x;
          normals[normalIndex++] = normal.y;
          normals[normalIndex++] = normal.z;
        }

        if (vertexFormat.tangent) {
          tangents[tangentIndex++] = tangent.x;
          tangents[tangentIndex++] = tangent.y;
          tangents[tangentIndex++] = tangent.z;
          tangents[tangentIndex++] = tangent.x;
          tangents[tangentIndex++] = tangent.y;
          tangents[tangentIndex++] = tangent.z;
        }

        if (vertexFormat.bitangent) {
          bitangent = Cesium.Cartesian3.normalize(
            Cesium.Cartesian3.cross(normal, tangent, bitangent),
            bitangent,
          );
          bitangents[bitangentIndex++] = bitangent.x;
          bitangents[bitangentIndex++] = bitangent.y;
          bitangents[bitangentIndex++] = bitangent.z;
          bitangents[bitangentIndex++] = bitangent.x;
          bitangents[bitangentIndex++] = bitangent.y;
          bitangents[bitangentIndex++] = bitangent.z;
        }
      }
    }

    for (i = 0; i < slices; i++) {
      if (vertexFormat.normal) {
        normals[normalIndex++] = 0;
        normals[normalIndex++] = 0;
        normals[normalIndex++] = -1;
      }
      if (vertexFormat.tangent) {
        tangents[tangentIndex++] = 1;
        tangents[tangentIndex++] = 0;
        tangents[tangentIndex++] = 0;
      }
      if (vertexFormat.bitangent) {
        bitangents[bitangentIndex++] = 0;
        bitangents[bitangentIndex++] = -1;
        bitangents[bitangentIndex++] = 0;
      }
    }

    for (i = 0; i < slices; i++) {
      if (vertexFormat.normal) {
        normals[normalIndex++] = 0;
        normals[normalIndex++] = 0;
        normals[normalIndex++] = 1;
      }
      if (vertexFormat.tangent) {
        tangents[tangentIndex++] = 1;
        tangents[tangentIndex++] = 0;
        tangents[tangentIndex++] = 0;
      }
      if (vertexFormat.bitangent) {
        bitangents[bitangentIndex++] = 0;
        bitangents[bitangentIndex++] = 1;
        bitangents[bitangentIndex++] = 0;
      }
    }
  }

  const numIndices = 12 * slices - 12;
  const indices = Cesium.IndexDatatype.createTypedArray(numVertices, numIndices);

  let index = 0;

  let j = 0;

  for (i = 0; i < slices - 1; i++) {
    indices[index++] = j;
    indices[index++] = j + 2;
    indices[index++] = j + 3;

    indices[index++] = j;
    indices[index++] = j + 3;
    indices[index++] = j + 1;

    j += 2;
  }

  indices[index++] = twoSlices - 2;
  indices[index++] = 0;
  indices[index++] = 1;
  indices[index++] = twoSlices - 2;
  indices[index++] = 1;
  indices[index++] = twoSlices - 1;

  // for (i = 1; i < slices - 1; i++) {
  //  indices[index++] = twoSlices + i + 1;
  //  indices[index++] = twoSlices + i;
  //  indices[index++] = twoSlices;
  // }

  // for (i = 1; i < slices - 1; i++) {
  //  indices[index++] = threeSlices;
  //  indices[index++] = threeSlices + i;
  //  indices[index++] = threeSlices + i + 1;
  // }

  let textureCoordIndex = 0;

  if (vertexFormat.st) {
    const rad = Math.max(topRadius, bottomRadius);

    for (i = 0; i < numVertices; i++) {
      const position = Cesium.Cartesian3.fromArray(positions, i * 3, positionScratch);

      st[textureCoordIndex++] = (position.x + rad) / (2.0 * rad);
      st[textureCoordIndex++] = (position.y + rad) / (2.0 * rad);
    }
  }

  const attributes = new Cesium.GeometryAttributes();

  if (vertexFormat.position) {
    attributes.position = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.DOUBLE,
      componentsPerAttribute: 3,
      values: positions,
    });
  }

  if (vertexFormat.normal) {
    attributes.normal = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 3,
      values: normals,
    });
  }

  if (vertexFormat.tangent) {
    attributes.tangent = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 3,
      values: tangents,
    });
  }

  if (vertexFormat.bitangent) {
    attributes.bitangent = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 3,
      values: bitangents,
    });
  }

  if (vertexFormat.st) {
    attributes.st = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 2,
      values: st,
    });
  }

  radiusScratch.x = length * 0.5;
  radiusScratch.y = Math.max(bottomRadius, topRadius);

  const boundingSphere = new Cesium.BoundingSphere(
    Cesium.Cartesian3.ZERO,
    Cesium.Cartesian2.magnitude(radiusScratch),
  );

  if (Cesium.defined(weatherVolumeConeGeometry._offsetAttribute)) {
    length = positions.length;
    const applyOffset = new Uint8Array(length / 3);
    const offsetValue = weatherVolumeConeGeometry._offsetAttribute === Cesium.GeometryOffsetAttribute.NONE
      ? 0
      : 1;

    Cesium.arrayFill(applyOffset, offsetValue);
    attributes.applyOffset = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.UNSIGNED_BYTE,
      componentsPerAttribute: 1,
      values: applyOffset,
    });
  }

  // eslint-disable-next-line consistent-return
  return new Cesium.Geometry({
    attributes,
    indices,
    primitiveType: Cesium.PrimitiveType.TRIANGLES,
    boundingSphere,
    offsetAttribute: weatherVolumeConeGeometry._offsetAttribute,
  });
};

let unitVolumeConeGeometry;

/**
 * Returns the geometric representation of a unit cylinder, including its vertices, indices, and a bounding sphere.
 * @returns {Cesium.Geometry} The computed vertices and indices.
 *
 * @private
 */
WeatherVolumeConeGeometry.getUnitCylinder = function () {
  if (!Cesium.defined(unitVolumeConeGeometry)) {
    unitVolumeConeGeometry = WeatherVolumeConeGeometry.createGeometry(
      new WeatherVolumeConeGeometry({
        topRadius: 1.0,
        bottomRadius: 1.0,
        length: 1.0,
        vertexFormat: Cesium.VertexFormat.POSITION_ONLY,
      }),
    );
  }

  return unitVolumeConeGeometry;
};
export default WeatherVolumeConeGeometry;
